/**
 * @name Unused global variable
 * @description Global variable is defined but not used
 * @kind problem
 * @tags efficiency
 *       useless-code
 *       external/cwe/cwe-563
 * @problem.severity recommendation
 * @sub-severity low
 * @precision high
 * @id py/unused-global-variable
 */

import python
import Definition

/**
 * Whether the module contains an __all__ definition,
 * but it is more complex than a simple list of strings
 */
predicate complex_all(Module m) {
    exists(Assign a, GlobalVariable all |
        a.defines(all) and a.getScope() = m and all.getId() = "__all__"
    |
        not a.getValue() instanceof List
        or
        exists(Expr e | e = a.getValue().(List).getAnElt() | not e instanceof StrConst)
    )
    or
    exists(Call c, GlobalVariable all |
        c.getFunc().(Attribute).getObject() = all.getALoad() and
        c.getScope() = m and
        all.getId() = "__all__"
    )
}

predicate unused_global(Name unused, GlobalVariable v) {
    not exists(ImportingStmt is | is.contains(unused)) and
    forex(DefinitionNode defn | defn.getNode() = unused |
        not defn.getValue().getNode() instanceof FunctionExpr and
        not defn.getValue().getNode() instanceof ClassExpr and
        not exists(Name u |
            // A use of the variable
            u.uses(v)
        |
            // That is reachable from this definition, directly
            defn.strictlyReaches(u.getAFlowNode())
            or
            // indirectly
            defn.getBasicBlock().reachesExit() and u.getScope() != unused.getScope()
        ) and
        not unused.getEnclosingModule().getAnExport() = v.getId() and
        not exists(unused.getParentNode().(ClassDef).getDefinedClass().getADecorator()) and
        not exists(unused.getParentNode().(FunctionDef).getDefinedFunction().getADecorator()) and
        unused.defines(v) and
        not name_acceptable_for_unused_variable(v) and
        not complex_all(unused.getEnclosingModule())
    )
}

from Name unused, GlobalVariable v
where
    unused_global(unused, v) and
    // If unused is part of a tuple, count it as unused if all elements of that tuple are unused.
    forall(Name el | el = unused.getParentNode().(Tuple).getAnElt() | unused_global(el, _))
select unused, "The global variable '" + v.getId() + "' is not used."
